#ifndef SIMPLE_IO_OPEN_H
#define SIMPLE_IO_OPEN_H

#include <utility> // std::move
#include <type_traits> // std::integral_constant std::bool_constant std::is_same_v std::true_type
#include <variant> // std::variant std::visit
#include <string> // std::string

#include "simple/support/meta/list.hpp" // support::meta::list_size_v support::meta::list
#include "simple/support/meta/find.hpp" // support::meta::find_v
#include "simple/support/meta/bind.hpp" // support::meta::bind
#include "simple/support/meta/remove_if.hpp" // support::meta::remove_if_t
#include "simple/support/meta/pend.hpp" // support::meta::prepend_t
#include "simple/support/function_utils.hpp" // support::overload

#include "permissions.h" // allow permission_flags
#include "handle.h" // unique_handle interface operation
#include "error.h" // error
#include "open_execute_common.h" // *

namespace simple::io
{
	namespace meta = support::meta;

	struct already_exists : error {};
	struct path_is_directory : error {};
	struct path_is_symbolic_link : error {};
	struct no_space_left_to_create : error {};
	struct file_too_big : error {};
	struct filesystem_is_read_only : error {};
	struct file_is_executing : error {};
	struct operation_not_supported : error {};

	using open_error_t = std::variant<
		access_denied,
		already_exists,
		path_is_directory,
		symbolic_link_loop,
		path_is_symbolic_link,
		process_descriptor_limit_reached,
		system_descriptor_limit_reached,
		path_component_length_exceeded,
		no_such_entity,
		no_such_directory,
		no_space_left_to_create,
		file_too_big,
		filesystem_is_read_only,
		file_is_executing,
		operation_not_supported,
		error
	>;

	using open_result = meta::prepend_t<open_error_t, unique_handle>;

	template <typename List, typename T>
	struct contains_t : std::bool_constant<
		meta::find_v<T, List> != meta::list_size_v<List>>
	{};

	class mode
	{
		enum class mode_value
		{
			read,
			write,
			create,
			exclusive,
			directory,
			truncate,
			append,
			nonblock,
			nofollow,
			sync_all,
			sync_data,
			execute_pass,

			direct,
			no_access_time
		};

		[[nodiscard]] static const char* get_message(mode_value) noexcept;

		unsigned flags;

		[[nodiscard]] static unsigned get_flag(mode_value) noexcept;
		[[nodiscard]] bool is_set(mode_value) noexcept;

		using read_t = std::integral_constant<mode_value, mode_value::read>;
		using write_t = std::integral_constant<mode_value, mode_value::write>;
		using create_t = std::integral_constant<mode_value, mode_value::create>;
		using exclusive_t = std::integral_constant<mode_value, mode_value::exclusive>;
		using directory_t = std::integral_constant<mode_value, mode_value::directory>;
		using truncate_t = std::integral_constant<mode_value, mode_value::truncate>;
		using append_t = std::integral_constant<mode_value, mode_value::append>;
		using nonblock_t = std::integral_constant<mode_value, mode_value::nonblock>;
		using nofollow_t = std::integral_constant<mode_value, mode_value::nofollow>;
		using sync_all_t = std::integral_constant<mode_value, mode_value::sync_all>;
		using sync_data_t = std::integral_constant<mode_value, mode_value::sync_data>;
		using execute_pass_t = std::integral_constant<mode_value, mode_value::execute_pass>;
		using direct_t = std::integral_constant<mode_value, mode_value::direct>;
		using no_access_time_t = std::integral_constant<mode_value, mode_value::no_access_time>;

		public:

		constexpr static read_t read{};
		constexpr static write_t write{};
		constexpr static create_t create{};
		constexpr static exclusive_t exclusive{};
		constexpr static directory_t directory{};
		constexpr static truncate_t truncate{};
		constexpr static append_t append{};
		constexpr static nonblock_t nonblock{};
		constexpr static nofollow_t nofollow{};
		constexpr static sync_all_t sync_all{};
		constexpr static sync_data_t sync_data{};
		constexpr static execute_pass_t execute_pass{};
		constexpr static direct_t direct{};
		constexpr static no_access_time_t no_access_time{};

		template <mode_value... modes,
			typename ModesList = meta::list<std::integral_constant<mode_value, modes>...>,
			template <typename...> typename has = meta::bind<contains_t, ModesList>::
				template bound
		>
		constexpr
		mode(std::integral_constant<mode_value, modes>...) : flags((get_flag(modes) | ...))
		{
			static_assert(sizeof...(modes) != 0);

			// !(bool >> bool) is implication ->
			static_assert(!( has<exclusive_t>{} >> has<create_t>{} ),
				"if exclusive, then create");
			static_assert(!( has<create_t>{} >> has<write_t>{} ),
				"if create, then write");
			static_assert(!( (has<truncate_t>{} || has<append_t>{}) >> has<write_t>{} ),
				"if truncate or append, then write");
			static_assert(not( has<truncate_t>{} && has<append_t>{} ),
				"can't append and truncate");
			static_assert(has<read_t>{} || has<write_t>{},
				"must read or write");
			static_assert(not(has<sync_all_t>{} && has<sync_data_t>{}),
				"sync_all implies sync_data");
			static_assert(!( has<no_access_time_t>{} >> has<read_t>{} ),
				"if not updating access time, then must be reading");
		}

		template <mode_value mode>
		[[nodiscard]] bool is_set(std::integral_constant<mode_value, mode>) noexcept
		{ return is_set(mode); }

		template <typename ModeList, typename Error>
		friend class bad_error_for_modes;

		template <mode::mode_value... modes>
		friend auto open(const std::string& path, permission_flags perms) noexcept;
		template <mode::mode_value... modes>
		friend auto open(meta::list<const std::integral_constant<mode::mode_value, modes>&...>, const std::string& path, permission_flags perms) noexcept;
		template <mode::mode_value... modes>
		friend auto open(const std::string& path) noexcept;
		template <mode::mode_value... modes>
		friend auto open(meta::list<const std::integral_constant<mode::mode_value, modes>&...>, const std::string& path) noexcept;

		template <mode_value... modes,
			typename ModesList = meta::list<std::integral_constant<mode_value, modes>...>,
			template <typename...> class has = meta::bind<contains_t, ModesList>:: template bound>
		static constexpr auto operations() { return has<mode::directory_t>{} ? operation::none :
			(has<mode::read_t>{} ? operation::read : operation::none) |
			(has<mode::write_t>{} ? operation::write : operation::none); }

		template <mode::mode_value... modes>
		[[nodiscard]] friend
		std::string get_message(meta::list<const std::integral_constant<mode::mode_value, modes>&...>)
		{ return "simple::io::mode list: |" + ((mode::get_message(modes) + std::string("|")) + ...); }
	};

	template <typename ModeList, typename Error>
	class bad_error_for_modes
	{
		template <typename T> using has = typename
		meta::bind<contains_t, ModeList>::template bound<T>;

		public:
		constexpr static bool value =
			std::is_same_v<Error, already_exists> &&
				not(has<mode::create_t>{} && has<mode::exclusive_t>{})
			||
			std::is_same_v<Error, path_is_directory> && has<mode::directory_t>{}
			||
			std::is_same_v<Error, symbolic_link_loop> && has<mode::nofollow_t>{}
			||
			std::is_same_v<Error, path_is_symbolic_link> && not(has<mode::nofollow_t>{})
			||
			std::is_same_v<Error, no_space_left_to_create> && not(has<mode::create_t>{})
			||
			std::is_same_v<Error, filesystem_is_read_only> && not(has<mode::write_t>{})
			||
			std::is_same_v<Error, file_is_executing> && not(has<mode::write_t>{})
		;
	};

	[[nodiscard]] open_result open(const std::string&, mode, permission_flags) noexcept;

	template <mode::mode_value... modes>
	[[nodiscard]] auto open(const std::string& path, permission_flags perms) noexcept
	{
		using mode_list = meta::list<std::integral_constant<mode::mode_value, modes>...>;
		using error_t = meta::remove_if_t<open_error_t, meta::bind<bad_error_for_modes, mode_list>::
			template bound>;
		using success_t = interface<mode::operations<modes...>()>;
		using result_t = meta::prepend_t<error_t, success_t>;
		return std::visit(support::overload{
				[](unique_handle&& x) -> result_t { return success_t{std::move(x)}; },
				[](auto&& x) -> result_t { return std::move(x); },
			},
			open(path, mode(std::integral_constant<mode::mode_value, modes>{}...), perms));
	}

	template <mode::mode_value... modes>
	[[nodiscard]] auto open(meta::list<const std::integral_constant<mode::mode_value, modes>&...>, const std::string& path, permission_flags perms) noexcept
	{ return open<modes...>(path, perms); };

	template <mode::mode_value... modes>
	[[nodiscard]] auto open(const std::string& path) noexcept
	{ return open<modes...>(path, allow() .owner().group().read().write() .others().read()); }

	template <mode::mode_value... modes>
	[[nodiscard]] auto open(meta::list<const std::integral_constant<mode::mode_value, modes>&...>, const std::string& path) noexcept
	{ return open<modes...>(path); };

	constexpr struct open_f_t
	{
		template <typename... Args>
		[[nodiscard]] decltype(auto) operator()(Args&& ... args) const noexcept
		{ return simple::io::open(std::forward<Args>(args)...); }
	} open_f;

	[[nodiscard]] inline std::string get_message(const open_f_t&)
	{ return "simple::io::open"; }

} // namespace simple::io

#endif /* end of include guard */
